import { ElementTypes, NodeTypes, root, node, elementNode, interpolationNode, textNode } from './ast'

/**parse的context上下文 */
interface context {
    /**一开始是原字符串，被一步步处理推进，都存放在这（被解析了的会删除，比如 {{message}}< /div>  ---> message}}< /div> ---> < /div> ） */
    source: string
}

/**tag标签的类型，分为开始和结束 */
const enum TagType {
    /**开始标签，比如 < div> */
    Start,
    /**结束标签，比如 </ div> */
    End,
}

/**基础解析 */
export function baseParse(content: string): root {
    const context = createParserContext(content)
    return createRoot(parseChildren(context, []))
}




/**解析孩子们，会循环解析
 * @param context 上下文
 * @param ancestors 祖先标签数组（栈）。 比如 《div》XXX《/div》中，XXX就是孩子，父标签是 div 的 elementNode
 * @returns 孩子数组
 */
function parseChildren(context: context, ancestors: elementNode[]): node[] {
    const nodes: node[] = []
    while (!isEnd(context, ancestors)) {
        const { source } = context
        let node: node | undefined
        if (source.startsWith("{{")) {//如果是以{{开始，才解析差值
            node = parseInterpolation(context)
        } else if (source[0] === '<') {//如果是以 < 开始
            if (/[a-z]/i.test(source[1])) {//如果<后是以字母开头（忽略大小写） 
                node = parseElement(context, ancestors)
            }
        }

        if (!node) { //否则就是text类型
            node = parseText(context)
        }
        if (node !== undefined) nodes.push(node)
    }

    return nodes
}
/**判断parseChildren是否结束
 * 1. source有值的时候
 * 2. 当遇到结束标签的时候 （因为在parseElement的时候，会调用parseChildren进行递归，最后才会删除结束标签。所以当遇到结束标签时就该停止了）
 * @param context 上下文
 * @param ancestors 需要判断的遇到的祖先element数组（栈），在parseElement函数中收集。  
 */
function isEnd(context: context, ancestors: elementNode[]) {
    const s = context.source

    if (s.startsWith(`</`)) {//如果是结束标签开头，那就需要弹出栈中数据
        for (let i = ancestors.length - 1; i >= 0; i--) {
            const { tag } = ancestors[i]
            if (startWithTagEndOpen(s, tag)) {
                return true
            }
        }
    }
    return !s
}

/**解析插值 */
function parseInterpolation(context: context): interpolationNode {
    // {{ message }}， 怎么提取出中间的message？ 获取 }} 的索引值，这样就能知道中间部分的长度了，就能提取出来了 。注意去掉空格

    /**插值的开始字符 */
    const openDelimiter = "{{"
    /**插值的结束字符 */
    const closeDelimiter = "}}"

    /**结束字符的开始位置 */
    const closeIndex = context.source.indexOf('}}', openDelimiter.length)//第二个参数设置从第几个开始查询，从 {{ 后开始查询。

    advanceBy(context, openDelimiter.length) //在这里去掉{{，进行推进 

    /**插值中的内容长度 */
    const rawContentLength = closeIndex - 2 //这里的2是 {{ 的长度
    /**截取出的原插值内容 （包含空格） */
    const rawContent = parseTextData(context, rawContentLength)
    /**真正的差值内容，不含空格 */
    const content = rawContent.trim()

    advanceBy(context, closeDelimiter.length) //解析成功之后，把}}部分删除（因为还有其它东西等着解析） 

    return {
        type: NodeTypes.INTERPOLATION,
        content: {
            type: NodeTypes.SIMPLE_EXPRESSION,
            content
        }
    }
}
/**解析element类型，会在里面调用parseChildren进行递归
 * @param context 上下文
 * @param ancestors 祖先标签数组（栈），在本函数中进行收集
 * @returns 
 */
function parseElement(context: context, ancestors: elementNode[]): elementNode {
    //1. 解析tag 
    //2. 删除处理完的代码

    const element = parseTag(context, TagType.Start) as elementNode //删除头标签
    ancestors.push(element)//收集标签
    element.children = parseChildren(context, ancestors)
    ancestors.pop()//解析完成后，从栈中弹出标签


    if (startWithTagEndOpen(context.source, element.tag)) {//判断当前的尾标签是否是我们要删除的，是才删除 。解决：<div><span></div>时，原本应该删除span尾标签，但是因为没写尾标签，导致直接删除了</div>，最终报错
        parseTag(context, TagType.End)//删除尾标签
    } else { //尾标签缺失时要报错
        throw new Error(`缺失结束标签：${element.tag}`)
    }



    return element
}
/**解析tag标签，并在context.source上删除。 
 * - 注：每调用一次，只会移除一半标签，所以一共需要调用两次。
 * 1. < div>hello</ div>  ---> hello</ div> 
 * 2. 调用其它处理方法，处理掉hello  --> </ div>
 * 2. </ div>  --->  '' 
 * @param context 上下文
 * @param type 类型，分为开始和结束，如果是开始才会返回内容
 * @returns 返回element类型的孩子（type是开始才会返回）
 */
function parseTag(context: context, type: TagType): elementNode | void {
    /**假设传入的是 < div>hello</ div>，得到是 ['<div', 'div', index: 0, input: '< div>hello</ div>', groups: undefined] */
    const match = /^<\/?([a-z]*)/i.exec(context.source) //以 <字母 开头  或  </字母 开头
    // console.log(match); //可以打印一下，这是个数组， 
    const tag = match?.[1] || '' //match[1]就是标签

    advanceBy(context, match?.[0]?.length || 0) //删除 <div  或  </div 
    //TODO 处理属性
    advanceBy(context, 1) //删除 >

    if (type === TagType.End) return //结束标签无需返回

    return {
        type: NodeTypes.ELEMENT,
        tag,
        tagType: ElementTypes.ELEMENT,
        // children: [
        //     {
        //         type: NodeTypes.TEXT,
        //         content: "hello",
        //     },
        // ],
    }


}
/**解析Text */
function parseText(context: context): textNode {
    //解析时的注意点： <div>hi,{{ msg }}</div>, 应该解析的是 hi, ，其它内容不能删除，交给其它函数解析
    /**结束解析时的截取长度，会根据是否遇到结束符来修改 */
    let endIndex = context.source.length
    /**结束符，遇到这个了就停止解析 */
    const endTokens = ['{{', '<']

    //判断是否有结束符，更新endIndex
    for (let i = 0; i < endTokens.length; i++) {
        /**结束符在context中的位置 */
        const endTokenIndex = context.source.indexOf(endTokens[i])
        //如果 结束符 在内容中存在，就需要更新endIndex。 
        // 注，取最小值，因为要尽可能靠左。 比如 <div><p>hi</p>{{ msg }}</div>，结束符应该是hi后面那个<，而不是插值 {{
        if (endTokenIndex !== -1 && endIndex > endTokenIndex) {
            endIndex = endTokenIndex
        }
    }


    //1. 获取content
    const content = parseTextData(context, endIndex)
    return {
        type: NodeTypes.TEXT,
        content,
    }
}

/**判断source中的结束标签，是否和我们想删除的结束标签相同
 * @param source  source
 * @param tag 想删除的标签
 * @returns 布尔值
 */
function startWithTagEndOpen(source: string, tag: string) {
    return source.startsWith('</') && source.slice(2, 2 + tag.length).toLowerCase() === tag //取出标签名， 比如 </div>  -->  div
}
/**截取context.source的指定长度，得到content，并且推进被截取的这部分 */
function parseTextData(context: context, length: number) {
    const content = context.source.slice(0, length)
    //2. 推进
    advanceBy(context, length)
    return content
}
/**推进context.source， 将会调用context.source.slice(length)，删除length之前的内容
 * @param context 要被推进的context
 * @param length 要被推进的长度
 */
function advanceBy(context: context, length: number) {
    context.source = context.source.slice(length)
}


/**创建根节点
 * @param children 
 * @returns 
 */
function createRoot(children: node[]): root {
    return {
        type: NodeTypes.ROOT,
        children
    }
}
/**创建上下文context
 * @param content 原字符串
 * @returns 
 */
function createParserContext(content: string): context {
    return {
        source: content
    }
}


